/**
 * 
 */
package spl.manager;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;
import javax.xml.transform.TransformerException;

import spl.manager.JobsContainer.Job;
import spl.utils.GlobalLogger;

/**
 * The class is responsible of the image stock and jobs assignment.
 * @author avikam
 *
 */
public class JobDistributor {
	private static String DEFAULT_TEMP_DIR = "photos";
	private static String DEFAULT_JOBS_DIR = "photos";
    
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public final Lock readLock = rwl.readLock();
    public final Lock writeLock = rwl.writeLock();	
	
	// Holds the root directory of the jobs
	
	// Holds images
	HashMap<Integer, ArrayList<Representation> > imagesHash;
	//ArrayList<Job> allJobs;
	JobsContainer allJobsContainer;
	private File tempDirectory;
	private File rootDir;
	
	/**
	 * C'tor - Initialize the parameters as follows:
	 * @param jobRootDir - is a path to the root directory in which the server
	 * hold the photos
	 * @param jobTmpDir - is a path to a temporary files that the server will
	 * hold temporary staff, like, a new upload before it is moved to the root dir.
	 */
	public JobDistributor(String jobRootDir, String jobTmpDir){
		this.tempDirectory = new File(jobTmpDir);
		if (!this.tempDirectory.isDirectory()){
			this.tempDirectory = new File(JobDistributor.DEFAULT_TEMP_DIR);
		}
		
		this.rootDir = new File(jobRootDir);
		if (!this.rootDir.isDirectory()){
			this.rootDir = new File(JobDistributor.DEFAULT_JOBS_DIR);
		}
		
		this.imagesHash = new HashMap<Integer, ArrayList<Representation> >();
		//this.allJobs = new ArrayList<Job>();
		
		this.allJobsContainer = new JobsContainer();
		
	}
	
	JobDistributor(){
		this(JobDistributor.DEFAULT_JOBS_DIR, JobDistributor.DEFAULT_TEMP_DIR);
	}
	
	/**
	 * Adds a job: save xml to disk, assign it to the proper queue
	 * @param resId resource's job
	 * @param xmlStream uploaded job
	 * @param fileSize size of xml file
	 * @return new job id, or INVALID_PRESENTATION if no suitable matched.
	 * @throws UnsupportedEncodingException 
	 */
	public Job addJob(int resId, InputStream xmlStream, int fileSize) throws UnsupportedEncodingException {
		// If no data, or no  such representation, return -1.		
		if (0 == fileSize || null == this.getPresentaionsFor(resId))
			return null;
		
		int repId = -1;
		File tmpFile = null;
		XMLParser jobDetails = null;
		BufferedReader xmlBuffer = new BufferedReader(new InputStreamReader(xmlStream, "UTF-8"));
		
		try {
			
			tmpFile = File.createTempFile("job", Integer.toString(resId), this.tempDirectory);
			PrintWriter pw = new PrintWriter(new FileWriter(tmpFile), true);
			
			String ln = xmlBuffer.readLine();
			int readyBytes = ln.length() + 1;
			while (readyBytes < fileSize) {
				pw.println(ln);
				
				// Read next line
				ln = xmlBuffer.readLine();
				readyBytes += ln.length() + 1;
			}
			
			if (ln != null && ln.length() != 0)
				pw.println(ln);
			
			pw.close();
			
			// Get presentation id
			jobDetails = new XMLParser(tmpFile.getPath());
			repId = jobDetails.getRepresentationId();
			
		} catch (IOException e) {
			GlobalLogger.getLogger().severe("IO Error while writing job");
			return null;			
		}
				
		GlobalLogger.getLogger().info("Adding Job for " + resId + ", " + repId);
				
		ArrayList<Representation> reps = this.imagesHash.get(resId);
		Representation oldRep = null; 
			
		try {
			this.writeLock.lock();
			oldRep = reps.get(repId);
			synchronized (reps) {
				// Return's new job id or -1 if failed
				int newJobId = this.allJobsContainer.getNextJobId();
				File newJobFile;
				// Create new representation
				Representation newRep = new Representation(resId, reps.size());
				
				Formatter jobFileName = new Formatter();
				jobFileName.format("job-%d-%d-%d.xml", resId, repId, newJobId);
				
				// Save new configuration (with representaion id)
				newJobFile = new File(this.rootDir, jobFileName.toString());
				jobDetails.setRepId(newRep.getId());
				jobDetails.dumpXML(newJobFile);
				
				tmpFile.delete();
				
				GlobalLogger.getLogger().info("Job Added! RES_ID=" + resId + ", REP_ID=" + repId + " JOB_ID=" + newJobId);
				
				// Create new job for the new representation
				Job addedJob = this.allJobsContainer.addJob(newJobId, newJobFile, 
																	oldRep, newRep); 
				
				oldRep.recordJob(addedJob);
				newRep.setCreatingJob(addedJob);

				// Keep track after representation list
				reps.add(newRep);
				
				return addedJob;
			}
		} catch (NullPointerException e) {
			GlobalLogger.getLogger().warning("Got bad arguemnts resoure id for adding job: " + repId + ", " + resId);
			return null;
		
		} catch (IndexOutOfBoundsException e) {
			GlobalLogger.getLogger().warning("Got bad arguemnts representation id for adding job: " + repId + ", " + resId);
			return null;
		}		
		catch (TransformerException e) {
				GlobalLogger.getLogger().severe("Couldn't save xml. Check out " + tmpFile.getPath());
				return null;
		}
		finally {
			this.writeLock.unlock();
		}
	}
	
	/**
	 * Adds new image interface. see addImage
	 * @param image is the buffer to upload
	 * @return see addImage
	 */
	public int addNewImage(ByteBuffer image) {
		return this.addImage(-1, -1, image);
	}
	
	/**
	 * Add image to stock.
	 * @param resId resource id to upload. -1 for new images
	 * @param repId representation to upload
	 * @param image buffer that holds the image
	 * @return true iff image uploaded to stock, false on:
	 *				1. Image couldn't be handled (IO Error)
	 *				2. Image is not a recognized image type (jpg) 						
	 */
	public int addImage(int resId, int repId, ByteBuffer image) {
		// Create temporary file to store the image
		File tmpFile;
		GlobalLogger.getLogger().finest("Got " + (image.capacity() - image.remaining()) + " buffer");
		try {
			tmpFile = File.createTempFile("upload", Integer.toString(resId), this.tempDirectory);
			
			FileOutputStream outputFile = new FileOutputStream (tmpFile, true);
		    FileChannel outChannel = outputFile.getChannel();
		    image.rewind();
		    outChannel.write(image);
		    outputFile.close();
		    
		} catch (IOException e) {
			GlobalLogger.getLogger().severe("IO Error on writing tmp file");
			return -1;
		}
	    
		// Get the file type
		String type = getImageType(tmpFile);
		if (null == type) {
			GlobalLogger.getLogger().severe("Got bad image type");
			tmpFile.delete();
			return -1;
		}
		
		ArrayList<Representation> reps = null;
		
		try {
			this.writeLock.lock();
			int nextResource;
			File dstFile;
			// Save in a proper name. Make sure images will not run
			// over each other.
			synchronized(this.imagesHash){
				if (-1 == resId) {
					reps = new ArrayList<Representation>();
					nextResource = this.nextImageID();
	
					this.imagesHash.put(nextResource, reps);
					dstFile = new File(this.rootDir, Integer.toString(nextResource) + "." + type);
			
					// Add first default representation
					reps.add(new Representation(nextResource, dstFile));
					
				} else {				
					try {
						reps = this.imagesHash.get(resId);
						
					} catch (IndexOutOfBoundsException e) {
						GlobalLogger.getLogger().info("Tried uploading invalid resource or rep");
						return -1;
					}
					
					Representation r = reps.get(repId);
					nextResource = resId;
					Formatter f = new Formatter();
					f.format("%d-%d.%s", nextResource, repId, type);
					dstFile = new File(this.rootDir, f.toString());
					
					r.setRepresentationFile(dstFile);
	                this.allJobsContainer.finishJob(r.getCreatingJob());
				}
				// Mostly, rename is an atomic operation
				// I.E: In Linux it's a single syscall: rename().
				tmpFile.renameTo(dstFile);
			
			}
			
			return nextResource;
		} finally {
			this.writeLock.unlock();
		}
	}

	/**
	 * Return the id of the next image we want to store
	 * @return valid id number.
	 */
	private int nextImageID() {
		return this.imagesHash.size();
	}
	
	private String getImageType(File img) {
		String line;
        Process p;
		try {
			p = Runtime.getRuntime().exec("file -b " + img.getPath());
			
	        // Initialize I/O
	        BufferedReader bri = new BufferedReader (new InputStreamReader(p.getInputStream()));

	        // Read output stream
	        line = bri.readLine();
	        bri.close();
	        
	        if (line.startsWith("JPEG"))
	        	return "jpg";
	        
	        if (line.startsWith("PNG"))
	        	return "png";
			
		} catch (IOException e) {
			GlobalLogger.getLogger().severe("IO Error on processing file type");
		}
        
        return null;
	}

	
	/**
	 * Simply a resource database view
	 * @param resId is the resource index
	 * @return list view of representations, or null if non such list.
	 */
	public List<Representation> getPresentaionsFor(int resId) {
		List<Representation> reps = this.imagesHash.get(resId);
		if (null != reps)
			return Collections.unmodifiableList(reps);
		
		return null;
	}
	
	/**
	 * @param resId resource id
	 * @param repId representation id
	 * @return a corresponding Representation, or null if non exists 
	 */
	public Representation getPresentaionFor(int resId, int repId) {
		List<Representation> reps = this.getPresentaionsFor(resId);
		
		
		if (null == reps)
			return null;
		
		Representation r;
		try {
			r = reps.get(repId);
		} catch (IndexOutOfBoundsException e) {
			return null;
		}
		
		return r;
	}
	/**
	 * @return a view only set of all resources
	 */
	public Set<Integer> getAllResources() {
		return Collections.unmodifiableSet(this.imagesHash.keySet());
	}
	
	/**
	 * @return a view only list of all jobs
	 */
	public List<Job> getAllJobs() {
		return this.allJobsContainer.getJobs();
	}
	
	/**
	 * @return new pending job. null if non such.
	 */
	public Job submitJob() {
		return this.allJobsContainer.submitJob();
	}
	
}
